Skip to content

feat(forecast_nwp): expose member= ensemble selector for GEFS/CFS (#74)#75

Merged
helloiamvu merged 5 commits into
merged-visionfrom
fix/issue-74-gefs-member-param
Jun 12, 2026
Merged

feat(forecast_nwp): expose member= ensemble selector for GEFS/CFS (#74)#75
helloiamvu merged 5 commits into
merged-visionfrom
fix/issue-74-gefs-member-param

Conversation

@helloiamvu

@helloiamvu helloiamvu commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the docs/API mismatch from #74: docs/forecasts.md documented a member= kwarg for GEFS ensemble selection, but forecast_nwp() never exposed it — the plumbing (_gefs_path(member=...), _cfs_path(member=...), build_fetch_plan(**per_model_kwargs)) supported member selection all along. This PR wires the public surface to it.

(Referenced issue: #74 — closing keyword lands in the release PR to main.)

What changed

  • mostlyright.weather.forecast_nwp.forecast_nwp — new keyword-only member: str | None = None. Validated early (before the lazy [nwp] imports, after the reserved/MSC/legacy gates) against _MEMBER_CAPABLE_MODELS = {"gefs", "cfs"} and the closed GEFS_MEMBERS / CFS_MEMBERS enums:
    • member= on any other model → ValueError naming the member-capable models.
    • Out-of-enum value → ValueError listing the sorted valid members.
    • Threaded to build_fetch_plan only when non-Nonemember=None (default) is byte-identical to pre-change behavior (path-builder defaults c00/01 apply). Both the single-cycle mirror loop and the multi-cycle _fetch_cycle recursion carry it.
  • mostlyright.forecasts.forecast_nwp (core wrapper) — accepts + forwards member (validation stays in the weather impl, single source of truth).
  • docs/forecasts.md — GEFS/CFS rows + new "Ensemble members" section; corrects the GEFS member-count wording (31-member ensemble c00+p01..p30, plus avg/spr statistical products = 33 enum values, previously "32 members").
  • No schema change — the output DataFrame does not grow a member column (selector only; noted in docs as future work).
  • No changes to _nwp_archive.py / _nwp_grids/ (already member-capable). RRFS declares members but its path builder isn't member-wired → intentionally excluded.

TS Parity

Same-PR parity per CROSS-SDK-SYNC Recipe A (no parity ticket needed): ForecastNwpOptions gains readonly member?: string with a doc comment mirroring the Python semantics. TS NWP remains the v2.0+ runtime-throw stub — signature-forward only, zero runtime bytes (interface + TSDoc erased by tsup). Compile-level test locks the options shape; exactOptionalPropertyTypes semantics verified (omission ≡ Python None).

Tests (TDD — RED confirmed before GREEN)

  • TestForecastNwpMember (8 tests, weather): non-member model rejection; invalid-member rejection listing the real enum; helper-level threading for gefs (p05) + cfs (03) via kwargs capture through the real build_fetch_plan + 404 MockTransport; member=None omits the kwarg (regression pin); pre-import ordering (valid member without [nwp] extra → SourceUnavailableError, not ValueError); multi-cycle recursion carries member; public single-cycle threading (lazy-import gate stubbed via sys.modules, helper kwargs captured, mutation-verified — deleting member=member at the call site fails it).
  • Core: wrapper forwards member verbatim (Mock on the delegation target).
  • TS: options-shape compile lock + stub still throws NwpNotAvailableError.

Review record (two-reviewer loop per REVIEW-DISCIPLINE.md, mixed-language → 3 reviewers)

Reviewer Iter 1 Iter 2
Codex (gpt-5.5, medium, read-only) PASS PASS
Python Architect REVISE — 1 HIGH PASS
TypeScript Architect PASS n/a (TS surface unchanged after iter 1)

The iter-1 HIGH: the public single-cycle member=member call site had zero test executions in any environment (CI lacks [nwp]; helper tests bypass the public fn; the multi-cycle test intercepts the recursion) — deleting it would silently fetch c00 while the suite stayed green. Fixed in 569c894 with a CI-safe public-path test; iter-2 architect independently reproduced the mutation kill and audited the patch.dict(sys.modules) restore semantics in both with/without-extra environments.

Verification

  • uv run pytest -m "not live" -q — full suite green (incl. parity gate, untouched)
  • uv run ruff check . + ruff format --check . — clean
  • pnpm -r run typecheck + pnpm -r run build — green
  • nwp-stub vitest suite — 8 passed; biome clean

🤖 Generated with Claude Code

Amendment (post-loop): cross-version skew guard

0753d9e — found while preparing the release: the core wrapper passed member=member unconditionally, so core 1.7.0 + a pre-member mostlyrightmd-weather<=1.6.0 (reachable outside the [research] extra) would TypeError on every forecast_nwp call. Fixed: member threads only when set (explicit member=None ≡ omission); [research] extra floor bumped to mostlyrightmd-weather>=1.7.0,<2.0 (mirrors the #64 variables= precedent, cff13b8); new core test pins the default-call omission. Amendment review: Codex + Python Architect re-dispatched (floor changes are never-skip) — Python Architect PASS (independently mutation-verified both directions; confirmed no other floor assertions; uv lock --check unaffected), Codex PASS.

minereda and others added 4 commits June 12, 2026 15:06
- Add keyword-only member: str | None = None to the weather impl
- Validate member EARLY (pre-[nwp]-import): reject on non-member models
  naming gefs/cfs; reject out-of-enum values listing sorted valid members
- Thread member to build_fetch_plan via per_model_kwargs only when non-None
  (member=None stays byte-identical — never overrides the path-builder default)
- Multi-cycle recursion forwards member to each single-cycle call
- TDD: TestForecastNwpMember (A-F) written RED first, then GREEN

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ity (#74)

- Core mostlyright.forecasts.forecast_nwp accepts member= and forwards it
  verbatim to the weather impl (validation stays in the weather package)
- TS ForecastNwpOptions exposes optional member?: string (signature-forward
  parity per the Dual-SDK rule; runtime stub still throws)
- TDD: core wrapper test written RED (TypeError) then GREEN; TS test locks
  the member? option shape at compile time

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…count (#74)

- GEFS row: 31-member ensemble (c00 + p01..p30) plus avg/spr statistical
  products — 33 values (was incorrectly '32 members')
- CFS row: document member= (01..04, default 01)
- Add an 'Ensemble members (member=)' section with GEFS/CFS examples,
  validation behavior, and the no-member-column scope note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… architect iter-1 HIGH)

The mirror-loop call site passing member= to _try_fetch_records_for_mirror
had zero test executions (CI lacks the [nwp] extra; Tests C/E hit the
helper directly; Test F intercepts the recursion). Stub the lazy [nwp]
imports into sys.modules and capture the helper kwargs through the public
forecast_nwp() entry point. Mutation-verified: deleting member=member at
the call site fails this test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Docs-required check: PASS

API-surface change includes docs updates — no reminder needed.

API-surface files changed:

packages-ts/weather/src/forecasts/nwp-stub.ts
packages/core/src/mostlyright/forecasts.py
packages/weather/src/mostlyright/weather/forecast_nwp.py

Docs files changed:

docs/forecasts.md

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Parity ticket gate: PASSED

parity-ticket-check: PR touches BOTH Python and TypeScript trigger surfaces — paired-language change satisfies the gate.

See CROSS-SDK-SYNC.md §2 for the workflow.

…r floor to 1.7.0 (#74)

Core 1.7.0 passing member=member unconditionally would TypeError on EVERY
forecast_nwp call against a pre-member mostlyrightmd-weather (<=1.6.0)
installed outside the [research] extra. Thread member only when set
(default calls stay skew-tolerant), pin the [research] extra to
weather>=1.7.0 (mirrors #64 codex P1 precedent for variables=), and pin
the default-call omission with a core-layer test mirroring weather Test D.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@helloiamvu helloiamvu merged commit 6172d8e into merged-vision Jun 12, 2026
21 checks passed
@helloiamvu helloiamvu deleted the fix/issue-74-gefs-member-param branch June 18, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants